package org.durandj.ystream.client.network;

import java.io.DataInputStream;
import java.io.IOException;
import java.net.UnknownHostException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;

import org.durandj.ystream.client.YStreamClient;
import org.durandj.ystream.client.message.Message;
import org.durandj.ystream.client.message.MessageHandler;
import org.durandj.ystream.client.message.MessageID;
import org.durandj.ystream.client.message.Message.Priority;
import org.durandj.ystream.client.network.packetHandlers.ClientDisconnectPacketHandler;
import org.durandj.ystream.client.network.packetHandlers.NOPPacketHandler;
import org.durandj.ystream.client.network.packetHandlers.PingPacketHandler;
import org.durandj.ystream.client.util.InetAddressUtils;
import org.durandj.ystream.client.util.Timer;

/**
 * Listens to the specified port for incoming messages
 * from clients and report it to any packet listeners.
 * 
 * @author durandj
 * @version 1.0
 *
 */
public class NetworkDaemon extends MessageHandler
{
	/**
	 * The socket for communicating with the server.
	 */
	private Server server;
	
	/**
	 * The packet manager.
	 */
	private PacketManager packetManager;
	
	/**
	 * The flag for keeping the daemon alive.
	 */
	private boolean isAlive;
	
	/**
	 * The thread of execution.
	 */
	private Thread thread;
	
	/**
	 * Creates a client network daemon that maintains the
	 * server connection and gets reads packets from the server.
	 * 
	 * @param serverAddress - the address of the server
	 * @param port - the port to connect through
	 * @throws IOException if something goes wrong with the connection process
	 * @throws UnknownHostException if the host could not be found
	 */
	public NetworkDaemon(String serverAddress, int port) throws UnknownHostException, IOException
	{
		super();
		
		this.packetManager = new PacketManager(128);
		this.isAlive = true;
		
		this.server = new Server(serverAddress, port);
		
		setupHandlers();
		
		this.thread = new Thread(new Runnable() {
			@Override
			public void run()
			{
				// We need to make sure some classes are up and running
				// before we do any work here so lets wait a moment.
				while(YStreamClient.getClient() == null)
					new Timer(100);
				
				while(YStreamClient.getClient().getNetworkDaemon() == null)
					new Timer(100);
				
				// We need to setup the network daemon as the recipient of messages from the server now.
				server.setWatchdog();
				
				while(isAlive)
				{
					// First let's check to see if there are any pending messages and
					// handle one of those.
					if(!noMessages())
						handleMessage(nextMessage());
					
					getPacket();
					
					new Timer(100);
				} // Main loop of the thread.
				
				// Let's do some cleanup work on the daemon.
				NetworkDaemon.this.server.getPacketSender().sendPacket(new Packet(Packet.Priority.HIGH,
				                                                                  Opcode.SERVER_DISCONNECT,
				                                                                  InetAddressUtils.getStringAddress(NetworkDaemon.this.server.getServerAddress()),
				                                                                  InetAddressUtils.getStringAddress(NetworkDaemon.this.server.getLocalAddress())));
				
				if(server.isAlive())
					server.killWatchdog();
				
				packetManager.stopHandlers();
			}
		}, "Network Daemon");
		thread.start();
	}
	
	/**
	 * Adds the packet handlers and their associated opcode.
	 * This should be modified whenever a new packet handler is
	 * added to the server.
	 */
	private void setupHandlers()
	{
		packetManager.setPacketHandler(Opcode.NOP, new NOPPacketHandler());
		packetManager.setPacketHandler(Opcode.CLIENT_DISCONNECT, new ClientDisconnectPacketHandler());
		packetManager.setPacketHandler(Opcode.PING, new PingPacketHandler());
	}
	
	/**
	 * Attempts to get a packet of data from the server.
	 */
	private void getPacket()
	{
		// We need a data stream reader and a buffer to store the stream contents in.
		DataInputStream reader = new DataInputStream(server.getInputStream());
		
		try
		{
			if(reader.available() == 0)
				return;
			
			byte tempbuf[] = new byte[Packet.MAX_PACKET_SIZE];
			ByteBuffer buf;
			
			// Here we read the stream.
			if(reader.read(tempbuf) == -1)
				return;
			
			buf = ByteBuffer.wrap(tempbuf);
			buf.rewind();
			
			try
			{
				// We can now try to assemble the packet.
				int opcode = buf.getInt();
				if(opcode < 0 || opcode >= packetManager.getOpcodeNumber())
				{
					log.warning("Bad packet data. Invalid opcode.");
					return;
				}
				
				int nPriority = buf.getInt();
				Packet.Priority priority = null;
				switch(nPriority)
				{
				case 0:
					priority = Packet.Priority.LOW;
					break;
					
				case 1:
					priority = Packet.Priority.MEDIUM;
					break;
					
				case 2:
					priority = Packet.Priority.HIGH;
					break;
					
				default:
					log.warning("Bad packet data. Invalid priority.");
					return;
				}
				
				// TODO: We could probably get rid of the size part of the packet!
				int size = buf.getInt();
				if(size < 0)
				{
					log.warning("Bad packet data. Invalid data size.");
					return;
				}
				
				// Get the contents of the packet.
				tempbuf = new byte[Packet.MAX_PACKET_DATA_SIZE];
				buf.slice().get(tempbuf);
				ByteBuffer contents = ByteBuffer.wrap(tempbuf);
				
				// Here's the final packet assembly.
				Packet packet = new Packet(priority,
				                           opcode,
				                           "127.0.0.1",
				                           InetAddressUtils.getStringAddress(server.getServerAddress()),
				                           contents);
				
				// Sending the packet to the packet handlers.
				packetManager.handlePacket(packet);
				
				// Since we've heard something from the server let's feed the watchdog.
				server.feedWatchdog();
			}
			catch(BufferUnderflowException e)
			{
				log.warning("Invalid packet data.");
				return;
			}
		}
		catch(IOException e)
		{
			log.warning("Unable to read data from server.");
			YStreamClient.getClient().queueMessage(new Message(Priority.HIGH, MessageID.DISCONNECT, this));
			return;
		}
	}
	
	/**
	 * Checks if the network daemon is alive.
	 * 
	 * @return the network daemon's status
	 */
	public boolean isAlive()
	{
		return isAlive || thread.isAlive();
	}
	
	/**
	 * Gets a reference to the server.
	 * 
	 * @return a server reference
	 */
	public Server getServer()
	{
		return server;
	}

	/* (non-Javadoc)
	 * @see org.durandj.ystream.server.message.MessageHandler#handleMessage(org.durandj.ystream.server.message.Message)
	 */
	@Override
	protected void handleMessage(Message msg)
	{
		switch(msg.getID())
		{
		case KILL:
			isAlive = false;
			break;
			
		case TIMER_EXPIRED:
			// The watchdog timer for the server connection has run out so lets disconnect from the server.
			log.info("Watchdog timed out for server.");
			YStreamClient.getClient().queueMessage(new Message(Priority.MEDIUM,
			                                                   MessageID.DISCONNECT,
			                                                   this));
			break;
			
		default:
			log.warning("Unhandled message!\n" +
						"\tID: " + msg.getID() + "\n" +
						"\tSender: " + msg.getSender().getClass().getSimpleName() + "\n");
			break;
		}
	}
}
